前兩天談了 Write(先存到外面)跟 Select(挑對的拉回來)。今天要處理的是第三個動作:就算內容確實需要留在 context 裡,能不能想辦法讓它佔用少一點空間?這就是 Compress。
最直接的做法:只保留最近幾輪,把太舊的內容直接砍掉。
def build_messages(history, max_turns=10):
return history[-max_turns:]
截斷簡單粗暴,代價也很直接,被砍掉的內容就是真的刪掉了。如果使用者在第 3 輪提到一個重要的名字,第 15 輪又問起,模型已經看不到那段對話,只能裝作不知道。
比較細膩的做法,是定期把「舊的內容」濃縮成重點摘要,取代原本一大串細節,而不是整個丟掉:
def summarize_history(client, old_messages):
conversation_text = "\n".join(
f"{m['role']}: {m['content']}" for m in old_messages
)
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=512,
messages=[
{
"role": "user",
"content": f"請把以下對話濃縮成重點摘要,保留關鍵資訊:\n\n{conversation_text}",
}
],
)
return response.content[0].text
# 當對話累積到一定長度後,把舊的部分換成摘要
summary = summarize_history(client, history[:-10])
messages = [
{"role": "user", "content": f"(先前對話摘要:{summary})"},
*history[-10:],
]
這個做法多花了一次額外的 API 呼叫(拿去做摘要本身也要花 token),換來的是「舊資訊還在,只是變得精簡」,比截斷保留更多東西,但實作起來也更複雜,摘要寫得好不好,會直接影響後面對話的品質,摘要漏掉的細節,跟被截斷砍掉的內容一樣,一樣是回不來的。
實際上通常不是每一輪都做壓縮,而是設一個觸發點,例如當 context 用量逼近上限的某個比例時,才觸發壓縮動作。像 Claude Code 這類工具,就會在 context 用量逼近上限(例如逼近 95%)時自動觸發類似的壓縮機制,讓對話可以繼續進行,而不會突然因為超過上限被打斷。這種「先讓它盡量自然累積,快滿了才處理」的做法,比每一輪都壓縮更省事,也避免了「內容明明還有很多空間,卻太早被壓縮掉」的問題。
Compress 跟昨天的 Select 常常會被放在一起討論,但處理的問題不一樣:Select 決定「這一輪要不要把某段內容拉進 context」,是一個「要不要」的判斷;Compress 處理的是「已經確定要留在 context 裡的內容」,怎麼讓它佔用的空間變小,是一個「怎麼縮」的問題。
Compress 的兩個基本手法是截斷跟摘要:截斷簡單但會真的遺忘,摘要保留較多重點但多一道加工、也多一次成本。實務上常見的做法是設一個觸發點(例如用量逼近上限時),而不是每一輪都做。
明天要談最後一個動作:Isolate——與其想辦法讓一個 context window 裝下所有東西,不如從架構上把內容拆到不同地方去。
那最好的壓縮方法是甚麼及如何更有效把所有內容打包到 context 內?
對了,我也是同系列的寫作者
https://ithelp.ithome.com.tw/users/20183569
請多多指教